Reading query and form fields in API calls

Misconception By design
Docly API endpoints (#/API/*.js) do not receive an Express-style request object — input arrives via auto-bound function parameters and the globals query (URL query string) and form (request body, JSON parsed for you).
Applies to: APIJavaScript

What you'll see

A developer — or more often an AI code-generation tool — writes an API endpoint as if it were running under Express or Node.js:

// #/API/hello.js — WRONG, all of it
export default function (req, res) {
    var name = req.query.name;        // TypeError — req is null
    var body = JSON.parse(request.body); // request has no body property
    res.json({ message: "Hello " + name }); // there is no res — return the value instead
}

None of this works. There is no req/res pair in Docly. A parameter named req is looked up as an input field called "req" — which doesn't exist — so it is null, and the first property access throws. The global request object exists, but it holds request metadata (URL, Jwt, file/folder info) — not the query string and not the body.

What's actually happening

When a request hits an #/API/ endpoint, Docly prepares the input before your function runs:

  • query (global) — every URL query-string parameter as a key/value map. All values are strings, always. ?page=2 gives query.page === "2", so parse numbers yourself.
  • form (global) — the request body:
    • For form posts (application/x-www-form-urlencoded or multipart/form-data), each form field becomes a string value: form.email, form.message, …
    • For JSON posts (Content-Type: application/json), Docly deserializes the entire body into form. Values keep their JSON types — nested objects, arrays, numbers and booleans come through as-is.
  • Auto-bound parameters — if the file exports a default (or anonymous) function, each parameter is bound by name from the input: form is checked first, then query. export default function (email, page) { ... } receives form.email/query.email and form.page/query.page automatically — whichever is present.

The same endpoint serves both GET and POST: on a GET there is simply no body, so form is empty and everything arrives in query.

Two details worth knowing:

  • If both form and query contain the same key, form wins in parameter binding.
  • A JSON body must be an object at the top level ({ ... }). Posting a bare array or scalar leaves form null — wrap the payload: { "items": [...] }.

What to do

Pick the simplest mechanism that fits, and never parse the body yourself.

Do — named parameters for simple endpoints (works for both GET and POST):

// #/API/greet.js
// GET  /API/greet?name=Ada           → name = "Ada" (from query)
// POST /API/greet  {"name": "Ada"}    → name = "Ada" (from JSON body)
export default function (name) {
    return { message: "Hello, " + (name || "stranger") + "!" };
}

Do — the query global for URL parameters (remember: values are strings):

// #/API/articles.js — GET /API/articles?page=2&tag=news
export default function () {
    var page = parseInt(query.page || "1");
    var tag = query.tag || null;
    return docly.getFiles("~/articles")
        .filter(a => !tag || a.Tag == tag)
        .slice((page - 1) * 20, page * 20);
}

Do — the form global for structured JSON bodies:

// #/API/orders.js
// POST /API/orders with Content-Type: application/json
// body: { "customer": { "name": "Ada", "email": "ada@example.com" }, "lines": [ ... ] }
export default function () {
    var customer = form.customer;     // nested object, types preserved
    var lines = form.lines || [];     // array comes through as an array
    if (!customer || !isEmail(customer.email)) throw "Invalid customer";
    return docly.saveFile("~/orders/" + guid() + ".json", { Content: JSON.stringify(form) });
}

Do — call it from the browser with ~/ paths:

fetch('~/API/orders', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ customer: { name: 'Ada', email: 'ada@example.com' }, lines: [] })
}).then(r => r.json()).then(result => console.log(result));

Don't:

export default function (req) { ... }      // no Express-style request argument exists
var body = JSON.parse(request.body);       // request has no body — JSON is already in form
var name = request.query.name;             // request has no query — use the query global

File uploads are the one exception: files posted as multipart/form-data do not appear in form. Use docly.getUploads() to list them and docly.getUploadBase64(fieldName) to read one. Ordinary text fields in the same multipart post still arrive in form.

query and form exist only in #/API/ scripts. Hash pages (.hash) are rendered once and cached, so they cannot read per-request input at all — route dynamic input through an API endpoint instead.